package com.wennn.top.security.provider;


import com.wennn.top.security.exception.TokenException;
import com.wennn.top.security.model.JwtAuthToken;
import com.wennn.top.security.model.SessionContext;
import com.wennn.top.security.model.SessionUser;
import com.wennn.top.security.model.token.JwtAccessToken;
import com.wennn.top.security.model.token.JwtRawAccessToken;
import com.wennn.top.security.service.SessionUserService;
import com.wennn.top.security.service.UserServiceImpl;
import com.wennn.top.security.vo.AuthUser;
import com.wennn.top.util.DateUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.ArrayList;
import java.util.Optional;

/**
 * 用户身份校验，token 解析用户信息
 * 1、若传入 username & password 则是第一次登陆认证
 * 2、若传入 token 则是验证前端token的有效性
 *
 * @author: wennn
 * @date: 5/9/20 3:43 PM
 */
@Slf4j
public class AuthProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    private PasswordEncoder bCryptPasswordEncoder;

    private SessionUserService sessionUserService;

    public AuthProvider(UserDetailsService userDetailsService, PasswordEncoder bCryptPasswordEncoder, SessionUserService sessionUserService) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.sessionUserService = sessionUserService;
    }

    // 判断token是否有效，有：继续，无：走登陆产生登陆
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        if (log.isDebugEnabled()) {
            log.debug("进入身份认证");
        }
        JwtAuthToken jwtAuthToken = (JwtAuthToken) authentication;

        JwtRawAccessToken accessToken = (JwtRawAccessToken) jwtAuthToken.getCredentials();
        // 若有JwtRawAccessToken 那么在这里解析一下token是否有效
        if (Optional.ofNullable(accessToken).isPresent()) {
            try {
                SessionContext sessionContext = sessionUserService.parseSession(accessToken);
                return new JwtAuthToken(sessionContext.getSessionUser(), accessToken, sessionContext.getAuthorities());
            } catch (ExpiredJwtException e) {
                log.error("Jwt Token 已过期 通过过期token获取Redis用户信息");
                // token 过期判断 redis 用户是否过期
                Optional<SessionContext> opSContext = sessionUserService.parseSessionUser(accessToken);

                if(opSContext.isPresent()){
                    if(log.isDebugEnabled()){
                        log.debug("JwtToken 过期，但Redis中的信息未过期。重新生成Token");
                    }
                    JwtAccessToken newJwtAccessToken = sessionUserService.create(opSContext.get());
                    return new JwtAuthToken(opSContext.get().getSessionUser(), accessToken, opSContext.get().getAuthorities());
                }

                log.debug("用户Redis中的信息也过期");
                throw new TokenException("Session 已过期");
            } catch (UnsupportedJwtException e) {
                log.error("Token格式错误: {} " + e);
                throw new TokenException("Token格式错误");
            } catch (MalformedJwtException e) {
                log.error("Token没有被正确构造: {} " + e);
                throw new TokenException("Token没有被正确构造");
            } catch (SignatureException e) {
                log.error("签名失败: {} " + e);
                throw new TokenException("签名失败");
            } catch (IllegalArgumentException e) {
                log.error("非法参数异常: {} " + e);
                throw new TokenException("非法参数异常");
            }
        }

        // 若 JwtRawAccessToken 为null 则通过JwtAuthToken 中的 用户名 & 密码 来认证
        String username = jwtAuthToken.getUsername();
        String password = jwtAuthToken.getPassword();

        if (log.isDebugEnabled()) {
            log.debug("第一次登陆进入数据库认证");
        }
        // 获取数据库用户
        AuthUser userDetails = Optional.ofNullable((AuthUser) userDetailsService.loadUserByUsername(username)).get();

        // 认证逻辑
        if (bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
            // 这里设置权限和角色
            ArrayList<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            // 生成令牌 这里令牌里面存入了:name,password,authorities, 当然你也可以放其他内容
            SessionUser sessionUser = SessionUser.builder(userDetails);
            SessionContext sessionContext = SessionContext.builder(sessionUser).setAuthorities(authorities);
            JwtAccessToken jwtAccessToken = this.sessionUserService.create(sessionContext);

            // 走密码第一次登陆的时候 缓存信息到redis中
            this.sessionUserService.cacheSessionUser(sessionUser.getKey(),sessionUser);

            JwtRawAccessToken jwtRawAccessToken = new JwtRawAccessToken(jwtAccessToken.getToken());
            sessionUser.setToken(jwtAccessToken.getToken());
            sessionUser.setLoginTime(DateUtil.nowLongTime());
            Authentication auth = new JwtAuthToken(sessionUser, jwtRawAccessToken, authorities);

            // 记录日志
            ((UserServiceImpl) userDetailsService).saveLoginAction(userDetails, true);

            return auth;
        } else {
            ((UserServiceImpl) userDetailsService).saveLoginAction(userDetails, true);
            throw new BadCredentialsException("密码错误");
        }

    }


    /**
     * 是否可以提供输入类型的认证服务
     *
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(JwtAuthToken.class);
    }

}
